package src.serverSide;

import java.util.ArrayList;
import java.util.List;

import src.categoryStructures.*;
import src.dataStructures.*;
import src.messagesToClient.*;

/**
 * This class structures and controls classes in the server
 * @author John Stewart
 */
public class Control {
	private PassLog passlog = new PassLog();
	private AccessLog accesslog = new AccessLog(passlog);
	private DataNames datanames = new DataNames();
	private AccountData accountdata = new AccountData();
	private ItemCategories itemcategories = new ItemCategories();
	
	/**
	 * Adds a new user, as long as the user name and password are appropriate
	 * @param address The Internet address of the communicating client
	 * @param username The user name of the created account
	 * @param password The new password
	 * @return Returns either the new user object or a helpful error message
	 */
	public MessageToClient newUser(String address, String username, String password) {
		if (datanames.exists(username)) return new NameTakenError();
		if (!datanames.checkNameFormat(username)) return new UserNameFormatError();
		if (!passlog.checkPasswordFormat(password)) return new PasswordFormatError();
		int userid = accountdata.newData(username, DataType.USER);
		passlog.addPass(userid, password);
		Data data = accountdata.getData(userid, DetailLevel.FULL);
		datanames.addName(username, userid, DataType.USER);
		accesslog.setUser(address, userid);
		if (data instanceof User) {
			return new GiveCurrentUser((User) data);
		} else System.out.println("Failure: new user item not a user");
		return new NullMessage();
	}
	
	/**
	 * This class creates a new item with the given name
	 * @param address The address of the author or the parent
	 * @param itemname The name to assign to the item
	 * @return A message with the new item, or the current user, or an error message
	 */
	public MessageToClient newItem(String address, String itemname) {
		if (datanames.exists(itemname)) return new NameTakenError();
		int userid = accesslog.getUserID(address);
		if (userid < 0) return new GiveCurrentUser(new User());
		int itemid = accountdata.newData(itemname, DataType.ITEM);
		datanames.addName(itemname, itemid, DataType.ITEM);
		accountdata.setParent(itemid, userid);
		accountdata.setChild(userid, itemid);
		accountdata.setPermission(itemid, accountdata.getData(userid, DetailLevel.EFFICIENT).permission);
		return new GiveData(accountdata.getData(itemid, DetailLevel.FULL));
	}
	
	/**
	 * This method makes a login attempt
	 * @param address The address to login in if successful
	 * @param username The user name of the login
	 * @param password The password to test
	 * @return The current user if successful; an error message if not
	 */
	public MessageToClient login(String address, String username, String password) {
		if (!datanames.exists(username)) return new NonexistantNameError(); // create some more useful exception message later
		int userid = datanames.getID(username);
		PassLog.LoginResult result = passlog.checkPass(userid, password); // more useful error message
		switch (result) {
		case invalidpassword:
			return new InvalidPasswordError();
		case invaliduserid:
			System.out.println("Failure: Username without ID in passlog during login attempt");
			return new NullMessage();
		case lockedlogin:
			return new LockedLoginError();
		case success:
			accesslog.setUser(address, userid);
			Data data = accountdata.getData(userid, DetailLevel.FULL);
			if (data instanceof User) {
				return new GiveCurrentUser((User) data);
			} else System.out.println("Failure: new user item not a user");
		}
		System.out.println("Failure: Switch failing to trigger during login attempt");
		return new NullMessage();
	}
	
	/**
	 * This method logs out for the given user
	 * @param address The address to log out of
	 * @return The new current user
	 */
	public MessageToClient logout(String address) {
		accesslog.removeUser(address);
		return new GiveCurrentUser(new User());
	}
	
	/**
	 * This method attempts to change the password
	 * @param address The source of the request
	 * @param username The user name of the login
	 * @param password The password to test
	 * @param newpassword The new password to store
	 * @return The response message, which is either an error or a new user
	 */
	public MessageToClient newPassword(String address, String username, String password, String newpassword) {
		if (!datanames.exists(username, DataType.USER)) return new NonexistantNameError();
		int userid = datanames.getID(username);
		PassLog.LoginResult result = passlog.setPass(userid, password, newpassword);
		switch (result) {
		case invalidpassword:
			return new InvalidPasswordError();
		case invaliduserid:
			System.out.println("Failure: Username without ID in passlog during password change attempt");
			return new NullMessage();
		case lockedlogin:
			return new LockedLoginError();
		case success:
			accesslog.setUser(address, userid);
			Data data = accountdata.getData(userid, DetailLevel.FULL);
			if (data instanceof User) {
				return new GiveCurrentUser((User) data);
			} else System.out.println("Failure: User item not a user");
		}
		System.out.println("Failure: Switch failing to trigger during password change attempt");
		return new NullMessage();
	}
	
	/**
	 * If the current user has permission over the listed user, that users account is unlocked
	 * @param address The source of the request
	 * @param userid The user ID to unlock
	 * @return The response, the requesters user account.
	 */
	public MessageToClient unlock(String address, int userid) {
		if (authorize(address, userid) > 0) passlog.unlock(userid);
		return getCurrentUser(address);
	}
	
	/**
	 * Returns the data associated by the ID.  The detail included depends on the requesters permission
	 * @param address The source of the request
	 * @param dataid The data ID that is requested
	 * @return A message containing the data requested
	 */
	public MessageToClient getData(String address, int dataid) {
		DetailLevel detail;
		if (authorize(address, dataid) > 0) detail = DetailLevel.FULL;
		else detail = DetailLevel.PUBLIC;
		return new GiveData(accountdata.getData(dataid, detail));
	}
	
	/**
	 * Returns a message containing the user associated with the login address
	 * @param address The address to obtain data on
	 * @return The message containing the user data
	 */
	public MessageToClient getCurrentUser(String address) {
		int dataid = accesslog.getUserID(address);
		if (dataid >= 0) {
			Data data = accountdata.getData(dataid, DetailLevel.FULL);
			if (data instanceof User) return new GiveCurrentUser((User) data);
		}
		return new GiveCurrentUser(new User());
	}
	
	/**
	 * This method uses the given data to update the server
	 * @param address The source to validate
	 * @param data The data to use
	 * @return The data updated or an update of the users account
	 */
	public MessageToClient setData(String address, Data data) {
		if (authorize(address, data.id) > 0) return new GiveData(accountdata.setData(data));
		else return getCurrentUser(address);
	}
	
	/**
	 * Deletes the data associated with dataid, if the user has permission
	 * @param address The accessing address to check
	 * @param dataid The ID of the data to remove
	 * @return The data of the accessing user
	 */
	public MessageToClient removeData(String address, int dataid) {
		if (authorize(address, dataid) > 0) {
			Data data = accountdata.getData(dataid, DetailLevel.EFFICIENT);
			if (data instanceof User) removeUser(dataid);
			else removeItem(dataid);
		}
		return getCurrentUser(address);
	}
	
	/**
	 * This method changes the permission of the given object, if the user has the permission himself
	 * @param address The accessing source
	 * @param dataid The data ID to change the permission of
	 * @param permission The new permission to set
	 * @return The new data object, or the users account information
	 */
	public MessageToClient setPermission(String address, int dataid, int permission) {
		if (authorize(address, dataid, permission) > 0) 
			return new GiveData(accountdata.setPermission(dataid, permission));
		else return getCurrentUser(address);
	}
	
	/**
	 * This method obtains an item list corresponding to the parameters given
	 * @param categories The categories to search under
	 * @param start The start ID of the list
	 * @param length The length of the list, counting backwards
	 * @return A message containing the list of data
	 */
	public MessageToClient getItemCategory(Category categories, int start, int length) {
		List<Integer> dataids = itemcategories.getItemCategory(categories, start, length);
		return new GiveDataList(getDataList(dataids));
	}
	
	/**
	 * This method changes the categories for a particular item in the system
	 * @param address The accessing source
	 * @param itemid The ID of the item to change
	 * @param categories The new categories to impose
	 * @return The return message
	 */
	public MessageToClient setItemCategories(String address, int itemid, Categories categories) {
		if (authorize(address, itemid) <= 0 || !(accountdata.getData(itemid, DetailLevel.EFFICIENT) instanceof Item)) 
			return getCurrentUser(address);
		accountdata.setCategories(itemid, categories);
		itemcategories.removeItem(itemid);
		itemcategories.addItem(itemid, categories);
		return new GiveData(accountdata.getData(itemid, DetailLevel.FULL));
	}
	
	/**
	 * This method finds the number of names given by length and starting from start
	 * @param start The name to begin the search on
	 * @param length The length of the list
	 * @param type The type (user or item) of name to search for
	 * @return A message with the list
	 */
	public MessageToClient getNameList(String start, int length, DataType type) {
		List<Integer> dataids = datanames.getNameList(start, length, type);
		return new GiveDataList(getDataList(dataids));
	}
	
	/**
	 * This method finds all names that start with the string start
	 * @param start The substring to search for
	 * @param maxlength The maximum length of the list
	 * @param type The type of name to search for, user or item
	 * @return A message containing the list
	 */
	public MessageToClient getSubNameList(String start, int maxlength, DataType type) {
		List<Integer> dataids = datanames.getSubNameList(start, maxlength, type);
		return new GiveDataList(getDataList(dataids));
	}
	
	/**
	 * Checks whether the listed user has admin permission over the ID given.  
	 * @param address The requesting address
	 * @param dataid The ID to check
	 * @return The value of the permission.  A value greater than zero is admin permission.  
	 */
	private int authorize(String address, int dataid) {
		User user = getUser(address);
		Data data = accountdata.getData(dataid, DetailLevel.EFFICIENT);
		int userpermission = (user == null) ? 0 : user.permission;
		int datapermission = (data == null) ? 0 : data.permission;
		if (data instanceof Item && user != null && user.name == ((Item) data).author) userpermission++;
		return userpermission - datapermission;
	}
	
	/**
	 * Checks whether the listed user has admin permission over the ID given.  
	 * @param address The requesting address
	 * @param dataid The ID to check
	 * @param permission the new permission to check
	 * @return The value of the permission.  A value greater than zero is admin permission.  
	 */
	private int authorize(String address, int dataid, int permission) {
		User user = getUser(address);
		Data data = accountdata.getData(dataid, DetailLevel.EFFICIENT);
		int userpermission = (user == null) ? 0 : user.permission;
		int datapermission = (data == null) ? 0 : data.permission;
		if (data instanceof Item && user != null && user.name == ((Item) data).author) userpermission++;
		if (datapermission < permission) datapermission = permission;
		return userpermission - datapermission;
	}
	
	/**
	 * This class retrieves the user object associated with the requesting address, or a new user item
	 * if none is found
	 * @param address The accessing location to search for
	 * @return The representative user object
	 */
	private User getUser(String address) {
		Data data = accountdata.getData(accesslog.getUserID(address), DetailLevel.EFFICIENT);
		if (data instanceof User) return (User) data;
		else return new User();
	}
	
	/**
	 * Removes the user from the system
	 * @param userid The ID of the user to remove
	 */
	private void removeUser(int userid) {
		passlog.removeUser(userid);
		String username = accountdata.removeData(userid);
		datanames.removeName(username);
	}
	
	/**
	 * Removes the item from the system
	 * @param itemid The ID of the item to remove
	 */
	private void removeItem(int itemid) {
		String itemname = accountdata.removeData(itemid);
		itemcategories.removeItem(itemid);
		datanames.removeName(itemname);
	}
	
	/**
	 * This method returns a list of the data referenced by the list of IDs
	 * @param dataids The IDs to find
	 * @return A list of data corresponding to the IDs
	 */
	private List<Data> getDataList(List<Integer> dataids) {
		List<Data> datas = new ArrayList<Data>();
		for (int dataid : dataids) datas.add(accountdata.getData(dataid, DetailLevel.BRIEF));
		return datas;
	}
}
